今天開始就由小弟本人帶大家使用 trpc 摟~,為了讓大家快速感受 trpc的魅力, 這邊會推薦別人整理好的 T3 stack 做介紹,T3 stack算是筆者認為把 trpc 架構用的非常好的框架,而目前 trpc 生態中主流會是以 T3 stack 為主,還沒用過的朋友可以去看看 T3 app~
但讀者也不用擔心看不懂,這邊筆者會一一介紹T3 stack 架構內容~
跟使用 vite 起專案一樣,只要打以下的 cli 指令就會幫你創建你需要的 template,讀者只要一一根據提示選擇需要內容就OK~
npm create t3-app@latest
T3 stack 很貼心的部分是有提供非常多的專案工具選項, nextAuth、prisma 、 tailwindcss 以及非常重要的主題 trpc !!!,可以根據你的需求選擇你要的工具,這邊為了教學就全部都給他加進去XD,畢竟小孩才做選擇我全部都要哈哈。

簡單介紹一下選擇的套件~
介紹:他是一個做第三方登入的功能,搭配 next api route 實現 OAuth 2.0 驗證,保存第三方 user info 到你的 session 中。
網址: https://next-auth.js.org/
介紹:一個 type safe 的 ORM 框架,大部分有在用 trpc 的使用者都會去搭配使用。
網址: https://www.prisma.io/
介紹:trpc 的 client 端是封包 react query 的內容,所以 call api 方式會跟 react query一樣。
網址: https://tanstack.com/query/v5/
介紹:第一天有介紹喔~。
網址: https://trpc.io/
那各位讀者也不用擔心沒用過,這些日後都會慢慢介紹的~
建好專案後整體結構如下:
prisma : 整個 db 會用到的 schema都在這邊,同時所有 db migrate 紀錄都會在這邊出現,這邊得 migrate 資料呀,會根據你使用的 db 種類而有差異, prisma 整合非常多的 db 從SQl 到 noSQl 都有例如 Postgresql 甚至是 mongodb 等等,那 t3 stack 預設會是使用 postgresql,所以這邊的 migrations 內容都是 postgresql 的 SQL 指令。
src/pages/api/auth/[...nextauth] : 這邊是 nextauth 做第三方登入需要回傳驗證結果的 api route,細節部分日後再一一解說 code。
src/pages/api/auth/[trpc] : 這邊就是轉裡所有 trpc api 的入口, 還記得昨天提到 trpc 他是一個 client 跟 server 的設計模式吧,這邊就是 trpc 的 server 端入口,統一管理所有定義的 route、req contet、global error handler 等等。
server/auth : 所有關於 nextauth 的 option 設定
server/db : 這邊就是你會用到的 db instance,因為t3 stack是用 prisma 這邊就是放 prisma 得 instance。
server/db : 這邊就是你會用到的 db instance,因為t3 stack是用 prisma 這邊就是放 prisma 得 instance。
utils/api : 所有 api 的呼叫都在這邊。
env.mjs : 你可以在這邊定義環境變數,t3 app 很貼心的是他是透過 zod 去幫你驗證你的 env 是否有缺漏、內容是否有誤,所有 env的引用都會在這邊。
├── README.md
├── docker-compose.yml
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── prettier.config.cjs
├── prisma
│ ├── migrations
│ │ ├── 20230307161717_dev
│ │ │ └── migration.sql
│ │ ├── 20230307163349_
│ │ │ └── migration.sql
│ │ ├── 20230313151256_add_response_model
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ └── schema.prisma
├── public
│ └── favicon.ico
├── src
│ ├── env.mjs
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── api
│ │ │ ├── auth
│ │ │ │ └── [...nextauth].ts
│ │ │ └── trpc
│ │ │ └── [trpc].ts
│ │ ├── index.tsx
│ │ └── poll
│ │ └── [pollId].tsx
│ ├── server
│ │ ├── api
│ │ │ ├── root.ts
│ │ │ ├── routers
│ │ │ │ ├── example.ts
│ │ │ │ └── poll.ts
│ │ │ └── trpc.ts
│ │ ├── auth.ts
│ │ └── db.ts
│ ├── styles
│ │ └── globals.css
│ └── utils
│ └── api.ts
├── tailwind.config.cjs
└── tsconfig.json
相信大家看到這邊的 env 使用肯定會非常疑惑,放心我第一次看也是很不懂,這邊就一步一步帶大家使用。
這邊可以根據你 env 的內容變化去制定 schema rule。
DATABASE_URL: z.string().url(): 必須是 https或是 http開頭的 env 如 https://sample。
NODE_ENV: z.enum(["development", "test", "production"]): 指定 NODE_ENV 有什麼環境選項。
NEXTAUTH_SECRET : 根據你的 NODE_ENV dynamic 你的 schema rule。
NEXTAUTH_URL : preprocess 他是一個可以幫你轉換變數成你要的 value,他有兩個參數,第一個是轉換的 callback function,第二個則是 schema rule,整個流程會是,假設你部署到 vercel 那你的 env 中就會自動有 vercel 提供的 predefined 的 env VERCEL_URL 跟 VERCEL ,如果你是在 vercel 部署那就是替換 NEXTAUTH_URL 的 value 成 VERCEL_URL 的 value,反之則不變。
//.env
NEXTAUTH_URL="https://vercel.com/some_path"
NEXTAUTH_URL="http://localhost:3000"
const hasDeployToVercel = z.preprocess(
(str) => process.env.VERCEL_URL ?? str,
process.env.VERCEL ? z.string().min(1) : z.string().url(),
)
console.log(hasDeployToVercel.parse(process.env.NEXTAUTH_URL)) // "https://vercel.com/some_path"
//.env
NEXTAUTH_URL="http://localhost:3000"
const hasDeployToVercel = z.preprocess(
(str) => process.env.VERCEL_URL ?? str,
process.env.VERCEL ? z.string().min(1) : z.string().url(),
)
console.log(hasDeployToVercel.parse(process.env.NEXTAUTH_URL)) // "http://localhost:3000"
DISCORD_CLIENT_ID 跟 DISCORD_CLIENT_SECRET: string type。
const server = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
NEXTAUTH_SECRET:
process.env.NODE_ENV === "production"
? z.string().min(1)
: z.string().min(1).optional(),
NEXTAUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
// Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string().min(1) : z.string().url(),
),
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
DISCORD_CLIENT_ID: z.string(),
DISCORD_CLIENT_SECRET: z.string(),
});
在 next 中你除了可以定義 server env 外,也可以定義 client 端內容,兩者可以合而為一統一驗證,同時別忘記引入你所有的 env 喔這邊用 processEnv 代替。
const server = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
NEXTAUTH_SECRET:
process.env.NODE_ENV === "production"
? z.string().min(1)
: z.string().min(1).optional(),
NEXTAUTH_URL: z.preprocess(
(str) => process.env.VERCEL_URL ?? str,
process.env.VERCEL ? z.string().min(1) : z.string().url(),
),
DISCORD_CLIENT_ID: z.string(),
DISCORD_CLIENT_SECRET: z.string(),
});
const client = z.object({
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
});
const merged = server.merge(client);
const processEnv = {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,
};
所有 zod schema 的驗證都是呼叫前幾部定義好的 schema 並呼叫 parse function,但這邊採用 safeParse 原因是,safeParse 並不會 throw error出來,他只回傳一個 result,這個 result 包含 data 跟 status,相反 parse 如果驗證錯誤會直接 throw error 反之 return value,這邊會用 safeParse 是因為想客製化 error message 如果想改成 parse 也可以看大家的需求~
const isServer = typeof window === "undefined";
const parsed = /** @type {MergedSafeParseReturn} */ (
isServer
? merged.safeParse(processEnv) // on server we can validate all env vars
: client.safeParse(processEnv) // on client we can only validate the ones that are exposed
);
if (parsed.success === false) {
console.error(
"❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors,
);
throw new Error("Invalid environment variables");
}
簡單 demo 看出 safeParse 跟 parse 差別
import { z } from "zod";
const schema = z.object({
name: z.string()
})
let dataSuccess = {
name: 'danny'
}
let dataWrong = {
name: 10
}
const dataSuccessResult = schema.parse(dataSuccess)
// { name: 'danny' }
const dataWrongResult = schema.parse(dataWrong)
// error - ZodError: [
// {
// "code": "invalid_type",
// "expected": "string",
// "received": "number",
// "path": [
// "name"
// ],
// "message": "Expected string, received number"
// }
// ]
const dataSuccessResult1 = schema.safeParse(dataSuccess)
// { success: true, data: { name: 'danny' } }
const dataWrongResult2 = schema.safeParse(dataWrong)
// {
// success: false,
// error: [Getter],
// _error: ZodError: [
// {
// "code": "invalid_type",
// "expected": "string",
// "received": "number",
// "path": [
// "name"
// ],
// "message": "Expected string, received number"
// }
// ]
// }
jsDoc 並不是只會在 js 取寫,很多人可能以為 jsDoc 只是 typescript 出來前的替帶品,但其實兩者是可以一起使用的,好處就是 typescript 可以幫你做 type check,jsDoc 則是可以幫你的 code base 做詳細補充,但其實 jsDoc 也可以寫 type 喔~來看一下範例。
// 先定義 type ,用法就是 /** @typedef {your_type} your_type_name*/
/** @typedef {z.infer<typeof merged>} MergedOutput */
// 指定變數 type ,用法 /** @type {your_type_name}*/ , your_type_name 除了 typedef 定義的 name,以外也可以是 string 等原始 type 種類
let env = /** @type {MergedOutput} */(process.env)
這樣只要 hover env 變數就知道他有什麼 env 拉~

SKIP_ENV_VALIDATION env 決定要不要做 env validate
isServer 決定 safeParse 的 schema 用哪個parsed.success throw error
proxy 方式檢查 env 引用,讓使用 env 錯誤時有 log 來源if (!!process.env.SKIP_ENV_VALIDATION == false) {
const isServer = typeof window === "undefined";
const parsed = /** @type {MergedSafeParseReturn} */ (
isServer
? merged.safeParse(processEnv) // on server we can validate all env vars
: client.safeParse(processEnv) // on client we can only validate the ones that are exposed
);
if (parsed.success === false) {
console.error(
"❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors,
);
throw new Error("Invalid environment variables");
}
env = new Proxy(parsed.data, {
get(target, prop) {
if (typeof prop !== "string") return undefined;
// Throw a descriptive error if a server-side env var is accessed on the client
// Otherwise it would just be returning `undefined` and be annoying to debug
if (!isServer && !prop.startsWith("NEXT_PUBLIC_"))
throw new Error(
process.env.NODE_ENV === "production"
? "❌ Attempted to access a server-side environment variable on the client"
: `❌ Attempted to access server-side environment variable '${prop}' on the client`,
);
return target[/** @type {keyof typeof target} */ (prop)];
},
});
}
最後附上完整 code
// src/env.mjs
import { z } from "zod";
const server = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
NEXTAUTH_SECRET:
process.env.NODE_ENV === "production"
? z.string().min(1)
: z.string().min(1).optional(),
NEXTAUTH_URL: z.preprocess(
(str) => process.env.VERCEL_URL ?? str,
process.env.VERCEL ? z.string().min(1) : z.string().url(),
),
DISCORD_CLIENT_ID: z.string(),
DISCORD_CLIENT_SECRET: z.string(),
});
const client = z.object({
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
});
const processEnv = {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET,
};
const merged = server.merge(client);
/** @typedef {z.input<typeof merged>} MergedInput */
/** @typedef {z.infer<typeof merged>} MergedOutput */
/** @typedef {z.SafeParseReturnType<MergedInput, MergedOutput>} MergedSafeParseReturn */
let env = /** @type {MergedOutput} */ (process.env);
if (!!process.env.SKIP_ENV_VALIDATION == false) {
const isServer = typeof window === "undefined";
const parsed = /** @type {MergedSafeParseReturn} */ (
isServer
? merged.safeParse(processEnv) // on server we can validate all env vars
: client.safeParse(processEnv) // on client we can only validate the ones that are exposed
);
if (parsed.success === false) {
console.error(
"❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors,
);
throw new Error("Invalid environment variables");
}
env = new Proxy(parsed.data, {
get(target, prop) {
if (typeof prop !== "string") return undefined;
// Throw a descriptive error if a server-side env var is accessed on the client
// Otherwise it would just be returning `undefined` and be annoying to debug
if (!isServer && !prop.startsWith("NEXT_PUBLIC_"))
throw new Error(
process.env.NODE_ENV === "production"
? "❌ Attempted to access a server-side environment variable on the client"
: `❌ Attempted to access server-side environment variable '${prop}' on the client`,
);
return target[/** @type {keyof typeof target} */ (prop)];
},
});
}
export { env };
好了今天內容到這邊,明天會繼續陪大家研究其他資料夾部分,讀者如果有更多架構疑問可以下方留言一起討論喔~我們明天見
https://trpc.io/
https://tanstack.com/query/v5/
https://www.prisma.io/
https://next-auth.js.org/
✅ 前端社群 :
https://lihi3.cc/kBe0Y